Skip to content

Ios - #2

Merged
faustyu1 merged 8 commits into
mainfrom
ios
Jul 26, 2026
Merged

Ios#2
faustyu1 merged 8 commits into
mainfrom
ios

Conversation

@faustyu1

Copy link
Copy Markdown
Owner

No description provided.

faustyu1 and others added 8 commits July 26, 2026 19:19
Adds an iPhone/iPad client under ios/, built on NEPacketTunnelProvider so
every app on the device is tunnelled — not just proxy-aware ones.

The tunnel is Xray and nothing else. Xray-core v26 has a native layer-3
`tun` inbound with an iOS mode: when the `xray.tun.fd` environment flag is
set it adopts that descriptor instead of opening an interface itself. The
provider applies its network settings, finds the utun descriptor iOS just
created, and hands it straight to the core. No tun2socks, no local SOCKS
hop, no second core — packets go utun -> Xray -> server inside one process.

Shared code, not a fork. The iOS targets compile the very same
Sources/XrayClient/{Models,Core} files as the Mac app; platform-specific
pieces are behind `#if os(macOS)`. So parsers, config builder, routing
model, subscriptions, store, ping and localization cannot drift between
platforms, and `swift test` covers both.

Core changes:
- XrayConfigBuilder gains an inbound mode (local proxy vs. TUN), an
  optional log file, a DNS section with a port-53 rule, traffic stats, and
  a native WireGuard outbound.
- QRCode and DeviceID are now cross-platform; ServerStore and
  GeoAssetManager use the shared app group container on iOS.
- ProxyConfig.xraySupported marks the protocols an Xray-only build can run.
  Hysteria2/TUIC/AnyTLS need sing-box, so they are shown but not connectable.

iOS specifics:
- Switching servers does not reconnect the VPN: the app writes a new config
  and sends a `reload` message, the provider restarts only Xray on the same
  descriptor. Sub-second, and iOS never sees the tunnel drop.
- IPv6 is claimed by default so IPv6 traffic cannot route around the tunnel.
- NWPathMonitor rebuilds the core after Wi-Fi/cellular handovers and wake.
- Go heap capped at 48 MB — NetworkExtension processes are killed outright
  when they exceed their budget.

ios/XrayBridge is the gomobile binding; Scripts/ios/build-xraycore.sh
produces the (gitignored, ~110 MB static) XrayCore.xcframework, and
Scripts/ios/build-app.sh builds the app. CI added in .github/workflows/ios.yml.

Verified: simulator and device builds are clean, 48 shared tests pass, and
the exact configs the app generates boot a real Xray server through a passed
in tun fd (`XRAY_TUN_FD=1 xray run -test`) for Reality, XHTTP, WireGuard and
Shadowsocks+ws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The main screen offered "Add Subscription" and "Paste Link" side by side, which
made the user classify their own input before the app would take it. Now there
is a single "+" and a single sheet.

`AddInputClassifier` (shared Core, covered by tests) recognises share links one
per line, a wg-quick [Interface] profile, a subscription URL — plain or
base64-wrapped as some panels hand them out — and a base64 subscription body
pasted instead of its address. The sheet says what it found before you commit
("Subscription · panel.example.com", "2 servers detected") and keeps the confirm
button disabled for anything it can't place. The optional name field only
appears when the input is actually a subscription.

Also from checking the UI in the simulator:

- BalancerGrouper iterated a dictionary, so the server list reshuffled on every
  refresh. Bucket order now follows first appearance. Affected macOS equally.
- NEVPNError.localizedDescription is developer-speak; "IPC failed" is what the
  user saw when a tunnel refused to start. The actionable codes are now
  translated, including the Simulator's (which has no VPN stack at all).
- The status card ate half the screen before a single server was visible;
  tightened it.
- Labelled the DNS field in Settings — it was a bare, unexplained text row.
- The row badge for sing-box-only protocols said "Xray only", which reads as if
  the server *is* supported. It now says "needs sing-box".

Verified in the iOS 27 simulator: list, empty state, add sheet in all three
detection states, settings, routing, QR, light and dark. 58 shared tests pass;
simulator and device builds are clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every iOS screen was a mix of Russian and English. Three separate causes:

1. 55 of the strings the iOS app passes through `loc(…)` were never added to
   LocalizationTable. `Loc` falls back to the English key when a translation is
   missing, so the gap is invisible in development and only shows up to a user
   running the app in their own language.

2. The enum titles behind every picker — LogLevel, RoutingPreset (and its
   subtitles), AppAppearance, RuleOutbound, GeoAssetSource — returned hardcoded
   English and were never passed through `loc` at all, on either platform.
   "Bypass LAN", "Warning", "Proxy/Direct/Block" were English for everyone.

3. Strings the system draws itself (Cancel, Edit, the share sheet, the photo
   picker) stayed English regardless. Without .lproj bundles iOS treats the app
   as English-only, so it never offers its own localizations. Declaring
   CFBundleLocalizations fixes that; `EditButton` still follows the *device*
   language rather than this app's setting, so routing uses its own button.

83 keys added across all 11 languages, plus the 2 macOS settings strings that
were also missing. LocalizationTests now scans Sources/XrayClient/Views and
ios/App for `loc("…")` calls and fails if any key is absent, if a row is
missing a language, or if a row has an unknown language code — this class of
bug should not be found by a user again.

Also:

- The empty-state Add button showed a bare word: the button style drops a
  Label's icon unless the label style is stated explicitly.
- "Paste from clipboard" read UIPasteboard directly, which pops the system
  "Allow Paste?" alert on every tap and looks broken when declined. Replaced
  with a PasteButton, which needs no permission.
- Server counts are no longer interpolated into a translated noun; plural forms
  differ per language, so the detection footer reads "Servers found: N".

Verified in the simulator with -AppleLanguages (ru): main list, add sheet,
settings and routing are fully Russian, including system-drawn controls. 61
tests pass; simulator and device builds are clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The Connect/Disconnect button is gone; the shield itself is the control and
  fills a ring while held. Dropping the tunnel mid-session is disruptive, so a
  deliberate press beats a button that is easy to hit by accident. A caption
  under it says which way it goes, and it dims when the selected server is one
  the Xray-only build cannot run.
- Subscription note, traffic and expiry moved from the section footer to the
  header. Underneath the last row they read as if they described that server.
- The Test Ping button laid its icon out on a fixed column, leaving a gap
  before the title; it uses a plain HStack now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The subscription name, note, traffic and expiry floated on the list background
while everything around them sat on cards. They now share the same
secondarySystemGroupedBackground card as the rows, aligned to the same insets,
so a group reads as one object.

Collapsing is animated: the state change runs inside withAnimation, and the
chevron is a single glyph that rotates rather than two symbols swapping — the
latter cannot tween. Tapping anywhere across the name toggles it now, with the
overflow menu keeping its own hit area.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The separate backdrop read as a slab parked next to the server list rather than
part of it. The heading is now the section's first row, so the list draws one
rounded card around heading and servers together, with a separator between
them — one object instead of two.

The description is also no longer a wall of grey text: providers pad the
Announce header with blank lines, which ate the whole preview (the first two
lines were the title and an empty one), so blank lines are dropped and the note
shows two lines with tap-to-expand. Traffic and expiry share one compact line
with a 4pt bar drawn directly, since ProgressView's is too tall and too loud
for a line of metadata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two lines with tap-to-expand hid most of what the provider wrote. The note is
now rendered whole and is selectable. Runs of blank lines still collapse to a
single break — providers pad the Announce header with them, and the paragraphs
read better without the dead space.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tunnel needs the packet-tunnel-provider entitlement, which Apple only
issues with a paid Developer Program membership. Worth spelling out, because
both ways of trying to work around it fail confusingly: signing with a free
personal team is rejected outright, and stripping the entitlement produces an
app that installs and launches but whose VPN silently never starts. Re-signing
tools built on free accounts hit the same wall.

Adds a TON address for anyone who wants to help fund the membership, and notes
that nothing is paywalled and the licence stays MIT either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@faustyu1
faustyu1 merged commit 0cf14ef into main Jul 26, 2026
3 checks passed
@faustyu1
faustyu1 deleted the ios branch July 26, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant